package org.springframework.context.annotation;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.OrderComparator;
import org.springframework.stereotype.Component;


@Component
public class AnnotationConfigApplicationContext implements ApplicationContext {
	private static final Logger logger = Logger.getLogger(AnnotationConfigApplicationContext.class.getCanonicalName());
	
	private Set<Class<?>> beanClasses = new HashSet<Class<?>>();
	private Set<Object> beans = new HashSet<Object>();
	private ClassPathScanner scanner = new ClassPathScanner();
	
	public AnnotationConfigApplicationContext(String... basePackages) {
		beans.add(this);
		findBeanClasses("org.springframework");
		for(String basePackage : basePackages){
			findBeanClasses(basePackage);
		}
	}
	
	private List<SmartApplicationListener> smartApplicationListeners;
	public void publishEvent(ApplicationEvent event) {
		if(null == smartApplicationListeners){
			smartApplicationListeners = new ArrayList<SmartApplicationListener>(getBeansOfType(SmartApplicationListener.class).values());
			OrderComparator.sort(smartApplicationListeners);
		}
		for(SmartApplicationListener smartApplicationListener : smartApplicationListeners){
			if(smartApplicationListener.supportsEventType(event.getClass())){
				smartApplicationListener.onApplicationEvent(event);
			}
		}
	}
	
	private void findBeanClasses(String basePackage){
		Set<Class<?>> classes = scanner.getClassesInPackage(basePackage, null);
		for(Class<?> clazz : classes){
			if(null != clazz.getAnnotation(Component.class)){
				beanClasses.add(clazz);
			}
		}
	}

	@SuppressWarnings("unchecked")
	public <T> T getBean(Class<T> type) {
		boolean isPrototype = false;
		if(type.isAnnotationPresent(Scope.class)){
			Scope scope = type.getAnnotation(Scope.class);
			isPrototype = scope.value().equalsIgnoreCase("prototype");
		}
		
		if(!isPrototype){
			for(Object bean : beans){
				if(type.isAssignableFrom(bean.getClass())){
					return (T) bean;
				}
			}
		}
		
		for(Class<?> clazz : beanClasses){
			try {
				if(type.isAssignableFrom(clazz)){
					return (T) createBean(clazz);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return null;
	}
	
	private <T> T createBean(Class<T> clazz) throws Exception{
		logger.fine("Creating bean of type "+clazz.getSimpleName());
		T bean = clazz.newInstance();
		autowire(bean);
		initializeBean(bean);
		beans.add(bean);
		return bean;
	}
	
	private void initializeBean(Object bean){
		if(bean instanceof InitializingBean){
			InitializingBean initializingBean = (InitializingBean)bean;
			try {
				initializingBean.afterPropertiesSet();
			} catch (Exception e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}
		}
	}
	
	private void autowire(Object bean) throws Exception{
		Field[] fields = bean.getClass().getDeclaredFields();
		for(Field field : fields){
			field.setAccessible(true);
			if(null != field.getAnnotation(Autowired.class)){
				field.set(bean, getBean(field.getType()));
			}
		}
	}


	@SuppressWarnings("unchecked")
	public <T> Map<String, T> getBeansOfType(Class<T> type) {
		Map<String, T> map = new HashMap<String, T>();
		
		for(Class<?> clazz : beanClasses){
			try {
				if(type.isAssignableFrom(clazz)){
					
					boolean beanFound = false;

					for(Object bean : beans){
						if(clazz.equals(bean.getClass())){
							map.put(clazz.getCanonicalName(), (T)bean);
							beanFound = true;
						}
					}
				
					if(!beanFound){
						map.put(clazz.getCanonicalName(), (T)createBean(clazz));
					}
					
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		for(Object bean : beans){
			if(type.isAssignableFrom(bean.getClass())){
				map.put(bean.getClass().getCanonicalName(), (T)bean);
			}
		}
		
		return map;
	}
	
}
